Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题

您所在的位置:网站首页 malloc 内存对齐 Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题

Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题

#Linux系统下深究一个malloc/brk/sbrk新内存后的page fault问题| 来源: 网络整理| 查看: 265

有耳可听的,就应当听    —《马可福音》

周四的休假团建又没有去,不因别的,只因年前东北行休假太多了,想缓缓…不过真实原因也确实因为假期剩余无几了…思考了一些问题,写下本文。

  本文的缘起来自于和同事讨论一个关于缺页中断按需调页的讨论。真可谓是三人行必有我师,最近经常能从一些随意的比划或招架中悟出一丝意义,所以非常感谢周围的信息输出者!甚至从小小学校全员禁言的作业群里,我都能每天重温一首古诗词,然后循此生意,去故意制造另一种真实的意境,然后发个朋友圈?~

  感谢大家的信息输入,每次收到的好玩的东西,我都会即时整理并重新再输出。

内容简介

本文描述了一个非常显然但却又很少有人知道其所以然的问题,更重要的是分享一种解决问题的思路。PS:这个问题非常好玩。

  不搞悬念,本文解释一个事实,即匿名页缺页中断数量和物理页面的分配数量并不是一致的。即便不考虑共享内存的影响,也并非发生一次匿名页缺页中断,就一定会分配一个独立的物理页面。

问题

问题很简单,我把问题抽象成了下面的代码:

#include #include #include #include #define SIZE    100char *addrs[SIZE];char dest[4096][SIZE];int main() {        int i;        for (i = 0; i  pte 0000000037318067 # 翻译PUD表项   PTE     PHYSICAL  FLAGS37318067  37318000  (PRESENT|RW|USER|ACCESSED|DIRTY)

step 4:循着PUD表项读出PMD表项:

# 这里不再给注释crash> px (0xc7e000>>21) & 0x1ff$6 = 0x6 crash> px 0x37318000+$6*8$7 = 0x37318030 crash> rd -p 0x37318030        37318030:  000000003883d067                    g..8.... crash> pte 000000003883d067   PTE     PHYSICAL  FLAGS3883d067  3883d000  (PRESENT|RW|USER|ACCESSED|DIRTY)

step 5:循着PMD表项读出PTE:

crash> px (0xc7e000>>12) & 0x1ff$8 = 0x7e crash> px 0x3883d000+$8*8$9 = 0x3883d3f0 crash> rd -p 0x3883d3f0        3883d3f0:  800000003b9e3867                    g8.;.... crash> pte 800000003b9e3867       PTE         PHYSICAL  FLAGS800000003b9e3867  3b9e3000  (PRESENT|RW|USER|ACCESSED|DIRTY|NX)

此时的地址0x3b9e3000就是物理页面的位置了,由于我们brk申请了整个页面,因此页面偏移为0,接下来就是直接dump内存了。(严谨点讲,需要用 addr&0x0fff 找出页面偏移的,但是0xc7e000这个地址是页面对齐的,也就不再费事了!)

step 6:dump整个页面的内存:

crash> rd -p 0x3b9e3000        3b9e3000:  6161616161616161                    aaaaaaaa

哇!

step 7:看看p2对应的页表 由于p2和p3在虚拟地址上相邻4096个字节(一个页面),因此只需要做最后一步即可,即将p3的PTE索引向前移1个单位。但是我们依然按照正规的方式来一遍:

crash> px (0xc7d000>>12) & 0x1ff$10 = 0x7d crash> px 0x3883d000+$10*8$11 = 0x3883d3e8 crash> rd -p 0x3883d3e8        3883d3e8:  0000000000000000                    ........ crash> pte 0000000000000000 PTE  PHYSICAL  FLAGS 0       0     (no mapping) # nothing!!

连页表项都没有,何来的内容!请问何来的内容?!现在让我们的a.out向前一步走,即在a.out的运行终端敲入回车,再次dump p2的页表项。

step 8:a.out读取一下p2指针4096字节范围内的内容之后再次看p2的PTE:

crash> rd -p 0x3883d3e8        3883d3e8:  800000003df22225                     crash> pte 800000003df22225       PTE         PHYSICAL  FLAGS800000003df22225  3df22000  (PRESENT|USER|ACCESSED|NX)

看看吧,只要读了一下p2的内容,PTE就Present了!这个时候,我们可以读一下其内容:

crash> rd -p 0x3df22000        3df22000:  0000000000000000                    ........

内容为全0!

step 9:让a.out更进一步,拷贝’b’到p2后再次读取内容: 在a.out的终端上敲入回车,然后看PTE指示页面的内容:

crash> rd -p 0x3df22000        3df22000:  0000000000000000                    ........

这是为什么?为什么没有显示’b’字符,为什么还是全0?为此,不得不把最后一步重新来一遍了,即从读取PTE开始:

crash> px (0xc7d000>>12) & 0x1ff$13 = 0x7d crash> px 0x3883d000+$13*8 # 循着PMD找到PTE$14 = 0x3883d3e8 crash> rd -p 0x3883d3e8        3883d3e8:  800000002dac8867  # 注意!这个PTE和之前不同了crash> pte 800000002dac8867       PTE         PHYSICAL  FLAGS800000002dac8867  2dac8000  (PRESENT|RW|USER|ACCESSED|DIRTY|NX) crash> rd -p 0x2dac8000 # 对p2进行写操作前后,其PTE指示的页面不同了!!         2dac8000:  6262626262626262                    bbbbbbbb

重做一遍终于还是找到了,过程中PTE发生了变化。可以用vtop直接dump p2的虚拟地址看一下:

crash> vtop -c 1764 -u 0xc7d000 VIRTUAL     PHYSICAL         c7d000      2dac8000            PML: 38e20000 => 8000000037f4d067    PUD: 37f4d000 => 37318067    PMD: 37318030 => 3883d067    PTE: 3883d3e8 => 800000002dac8867   PAGE: 2dac8000       PTE         PHYSICAL  FLAGS800000002dac8867  2dac8000  (PRESENT|RW|USER|ACCESSED|DIRTY|NX)       VMA           START       END     FLAGS FILE ffff9be1eb39f440     c7d000     c7f000 8100073        PAGE       PHYSICAL      MAPPING       INDEX CNT FLAGS fffff3b700b6b200 2dac8000 ffff9be1c7c7eaf1      c7d  1 1fffff00080068 uptodate,lru,active,swapbacked# 结果正是2dac8000!和手工dump的结果完全一致!

我们前面绕了那么大一圈,其实直接用vtop命令就可以把整个MMU转换过程看得一清二楚。但是通过上述手工dump PTE的过程,更加熟悉了不是吗?

  但是这里出现一个问题!为什么在对p2进行只读操作时,和对它进行写入之后,其PTE指示的物理页面是不同的页面?

# 对p2内存只读操作之后  crash> rd -p 0x3883d3e8          3883d3e8:  800000003df22225    ————————————————  # 对p2内存写操作之后  crash> rd -p 0x3883d3e8          3883d3e8:  800000002dac8867

此外,其PTE对应的flags也不同,对其只读的时候,没有置入RW标志,即此时该页面是不可写的。

  是时候揭示谜底了!

  在对新分配的虚拟地址空间第一次读操作时,page fault确实会调入一个页面,然而对于这种读操作,所有的进程调入的都是同一个zeropage页面。对于这种第一次读操作,内核会将这同一个zeropage映射给被读的虚拟地址页面。这最大限度地发挥了Lazy策略的品性!

内核代码解释这一切(what->how)

到目前,我们已经知道所有事实,从用户态统计到内核crash工具分析,然而要想知道内核是如何做的,即从waht导出how,就要看一眼内核源码了,这里的目标非常明确,直接看do_anonymous_page的逻辑即可:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,        unsigned long address, pte_t *page_table, pmd_t *pmd,        unsigned int flags) {     ...    /* Use the zero-page for reads */     if (!(flags & FAULT_FLAG_WRITE)) {        // 只读情况直接从zeropage里拿即可。         entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),                         vma->vm_page_prot));         page_table = pte_offset_map_lock(mm, pmd, address, &ptl);        if (!pte_none(*page_table))            goto unlock;        goto setpte;     }     ...    // 非只读,才会实际从物理池里分配页面     page = alloc_zeroed_user_highpage_movable(vma, address);     ...    // 如果读fault映射了zeropage,将不会递增AnonRSS计数器。     inc_mm_counter_fast(mm, MM_ANONPAGES);     page_add_new_anon_rmap(page, vma, address); setpte:     ... }

我们看看什么是zeropage:

/*  * ZERO_PAGE is a global shared page that is always zero: used  * for zero-mapped memory areas etc..  */extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)];#define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))...static int __init init_zero_pfn(void) {     zero_pfn = page_to_pfn(ZERO_PAGE(0));    return 0; } ...static inline unsigned long my_zero_pfn(unsigned long addr) {    extern unsigned long zero_pfn;    return zero_pfn; }

所有谜底已经揭开!从一个实验用例,到统计分析,到crash工具分析内核状态,到内核源码确认,这就完成了一个解决问题的闭环,但貌似还缺少点什么…接下来还有一个形而上的分析。

how->why

之所以将第一次以read方式touch到的虚拟地址空间对应的物理页面映射到一个全局的zeropage,是在按需调页更进一步地加强了Lazy品性!从而更加有效地落实写时拷贝策略,将不得已而分配的物理页面真真地推迟到最后那一刻,从而将无谓的浪费行为降低到最少!

  如果说按需调页的page fault机制已经实现了Lazy品性,那么深究起来它做得还不够好,说它做得不够好是因为page fault机制忽略了按需调页两个层面中的一面:

当touch一个从未touch过的虚拟页面的时候,需要调入一个物理页面;

当调入一个物理页面时,是不是可以和其它的进程共享该物理页面;

第1点说的是按需分配,不得已时才分配,page fault做到了(注意,PTE本身也是按需调入的),第2点说的是尽力压缩,非要分配时,能不能尽量少分配,两者都做到了,Lazy策略才能达到真正按需调页思路的极致。

结论 & 评价

结论很明确:

Linux系统并不会对新分配未touch的虚拟内存映射任何物理页面;  以read方式首次touch时会映射共享的zeropage页面;  以write方式首次touch时会分配新的物理页面并映射之;  以write方式在首次read touch之后touch时,写时拷贝会分配新的物理页面并映射之。

为什么是这样可以考虑以下两点:

基于虚拟地址空间的操作系统内存子系统采用的是按需调页策略,这是设计决定的。

参考Linux内核的实现,Linux page fault处理区分对待了匿名页的read fault和write fault。

附:关于PTE的按需调入

在进程新fork出来初始化时,mm_init函数中会调用pgd_alloc,在pgd_alloc中会处理pgd的prepopulate,但是没有pte的populate,pte的populate是在do_page_fault中处理的,可见由于内存分布的局部稠密性和全局稀疏性,PTE的Lazy分配时必要的。

  其次,我们看C库增持内存池内存需要通过系统调用进一步调用到的do_brk以及do_mmap,除非你使用了VM_LOCKED,否则就不会prepopulate任何页面。上面两点保证了至少在X86的32位/64位平台,不会对用户地址空间的虚拟内存有任何可读的预映射页面。

  若要观测PTE本身的按需调入,参考下面的代码:

#include #include #include int main(int argc, char **argv) {        char *p1, *p2;        char c;        int i;         p1 = sbrk(0);        // 让p2离开更远的距离,防止p2的PTE和已分配的PTE在一个page内!         for (i = 0; i 


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3